
package model;

import java.awt.Font;
import java.util.Observable;
import java.util.Observer;

import javax.swing.DefaultListModel;

/**
 * CPU class for the simulator.
 * 
 * @author DoubleS
 * @version 1.0
 */
public class CPU extends Observable implements Observer, Runnable
{
  /**
   * The current counter.
   */
  private int my_counter;

  /**
   * The max counter.
   */
  private int my_max_counter;

  /**
   * Counter for the number of processes.
   */
  private int my_process_counter;

  /**
   * The scheduler.
   */
  private Scheduler my_scheduler;

  /**
   * The share memory.
   */
  private ShareMemory my_memory;

  /**
   * The running process.
   */
  private Process my_running_process;

  /**
   * The timer thread.
   */
  private SystemTimer my_timer;

  /**
   * The io 1 thread.
   */
  private IODevice my_io_1;

  /**
   * The io 2 thread.
   */
  private IODevice my_io_2;

  /**
   * Is the running process needed to be updated?
   */
  private boolean my_context_switch;
  private boolean my_run;

  /**
   * Construct an empty CPU.
   */

  public CPU()
  {
    super();
    my_timer = new SystemTimer();
    my_io_1 = new IODevice(0);
    my_io_2 = new IODevice(1);
    my_timer.addObserver(this);
    my_io_1.addObserver(this);
    my_io_2.addObserver(this);
    reset();
  }

  /**
   * Reset the CPU. Clear all the fields. Set the input to be the max counter.
   * 
   * @param the_max_counter The max counter.
   */
  public void reset()
  {
    my_counter = 0;
    my_max_counter = 0;
    my_process_counter = 0;
    my_scheduler = new Scheduler();
    my_memory = new ShareMemory();
    my_context_switch = true;
    my_run = true;
    my_timer = new SystemTimer();
    my_timer.addObserver(this);
    //my_timer.setIsDone(false);
    my_io_1.setIsDone(false);
    my_io_2.setIsDone(false);
  }

  /**
   * 
   */
  public void stop()
  {
    my_run = false;
  }

  /**
   * Set up the simulation.
   * 
   * @param the_max_counter The max counter.
   * @param the_process_list The process list.
   */
  public void setupSimulation(final int the_max_counter,
                              final DefaultListModel the_process_list)
  {
    setMaxCounter(the_max_counter);
    my_scheduler.createIOBQ(0);
    my_scheduler.createIOBQ(1);
    for (Object input_process : the_process_list.toArray())
    {
      switch (((InputProcess) input_process).getProcessType())
      {
        case CALCULATING:
          my_scheduler.addProcessToReadyQ(new Calculating("Cal" + my_process_counter));
          my_process_counter++;
          break;
        case IO:
          my_scheduler.addProcessToReadyQ(new IOProcess("IO" + my_process_counter));
          my_process_counter++;
          break;
        case PRODUCER_CONSUMER:
          addProducerConsumerPair();
          break;
        default:
          break;
      }
    }
  }

  public SystemTimer getTimer()
  {
    return my_timer;
  }

  public IODevice[] getIODevices()
  {
    IODevice[] devices = {my_io_1, my_io_2};
    return devices;
  }

  /**
   * Create and add a pair of producer and consumer to the scheduler's ready
   * queue.
   */
  private void addProducerConsumerPair()
  {
    final Semaphore read_ok =
        new Semaphore(0, "Read_OK_" + my_memory.getCounter(), my_memory.getCounter());
    my_memory.addSemaphore(read_ok);
    final Semaphore write_ok =
        new Semaphore(1, "Write_OK_" + my_memory.getCounter(), my_memory.getCounter());
    my_memory.addSemaphore(write_ok);
    final Producer a_producer =
        new Producer(read_ok.getID(), write_ok.getID(), "Producer" + my_process_counter);
    my_process_counter++;
    final Consumer a_consumer =
        new Consumer(read_ok.getID(), write_ok.getID(), "Consumer" + my_process_counter);
    my_process_counter++;
    my_scheduler.addProcessToReadyQ(a_producer);
    my_scheduler.addProcessToReadyQ(a_consumer);
    my_scheduler.createSemBQ(read_ok);
    my_scheduler.createSemBQ(write_ok);
  }

  /**
   * Set the max counter of the CPU.
   * 
   * @param the_max_counter The max counter.
   */
  public void setMaxCounter(final int the_max_counter)
  {
    my_max_counter = the_max_counter;
  }

  /**
   * The down trap service routine.
   * 
   * @param the_sem_id The semaphore.
   */
  public void downTSR(final int the_sem_id)
  {
    final Semaphore a_sem = my_memory.getSemaphore(the_sem_id);
    setChanged();
    notifyObservers("The process " + my_running_process.toString() +
                    " request a down on the semaphore " + a_sem.toString() + "\n");
    if (a_sem.getValue() == 0)
    {
      my_running_process.setStatus(Status.BLOCKED);
      my_scheduler.addProcessToSemBQ(a_sem, my_running_process);
      my_context_switch = true;
      setChanged();
      notifyObservers("The process " + my_running_process.toString() +
                      " is blocked by the semaphore " + a_sem.toString() + "\n");
    }
    else
    {
      a_sem.setValue(a_sem.getValue() - 1);
    }
  }

  /**
   * The up trap service routine.
   * 
   * @param the_sem_id The semaphore.
   */
  public void upTSR(final int the_sem_id)
  {
    final Semaphore a_sem = my_memory.getSemaphore(the_sem_id);
    setChanged();
    notifyObservers("The process " + my_running_process.toString() +
                    " request an up on the semaphore " + a_sem.toString() + "\n");
    if (a_sem.getValue() == 1)
    {
      // This should never happen! Because only one Producer associate with one
      // Consumer.
      my_running_process.setStatus(Status.BLOCKED);
      my_scheduler.addProcessToSemBQ(a_sem, my_running_process);
      my_context_switch = true;
      setChanged();
      notifyObservers("The process " + my_running_process.toString() +
                      " is blocked by the semaphore " + a_sem.toString() + "\n");
    }
    else
    {
      // check the blocked queue
      // release the first in the queue
      if (!my_scheduler.isSemBQempty(a_sem))
      {
        final Process a_process = my_scheduler.getNextProcessInSemBQ(a_sem);
        a_process.setStatus(Status.READY);
        my_scheduler.addProcessToReadyQ(a_process);
        setChanged();
        final StringBuffer a_buffer = new StringBuffer(200);
        a_buffer.append("The process " + a_process.toString() +
                        " is released from the semaphore blocked queue for " +
                        a_sem.toString());
        a_buffer.append("\nThe updated queues are : " + my_scheduler.toString() + "\n");
        notifyObservers(a_buffer.toString());
      }
      else
      {
        a_sem.setValue(a_sem.getValue() + 1);
      }
    }
  }

  /**
   * IO trap service routine.
   * 
   * @param the_action The action.
   */
  public void ioTSR(final ProcessAction the_action)
  {
    int the_id;
    IODevice the_io;
    if (the_action == ProcessAction.IO_REQUEST_1)
    {
      the_io = my_io_1;
      // my_io_1.receiveRequest();
      // the_id = my_io_1.getID();
    }
    else
    {
      the_io = my_io_2;
      // my_io_2.receiveRequest();
      // the_id = my_io_2.getID();
    }
    setChanged();
    notifyObservers("The process " + my_running_process.toString() + " request the " +
                    the_io.getID() + " IO device");
    my_running_process.setStatus(Status.BLOCKED);
    my_scheduler.addProcessToIOBQ(the_io.getID(), my_running_process);
    my_context_switch = true;
    setChanged();
    notifyObservers("The process " + my_running_process.toString() + " is blocked by the " +
                    the_io.getID() + " IO device");
    setChanged();
    notifyObservers("The updated queues are : " + my_scheduler.toString());
    the_io.receiveRequest();
  }

  /**
   * Do the action base on the ProcessAction.
   * 
   * @param the_action The ProcessAction.
   */
  public void doAction(final ProcessAction the_action)
  {
    switch (the_action)
    {
      case DOWN_ON_READ:
        downTSR(((Consumer) my_running_process).getReadOkSemID());
        break;
      case DOWN_ON_WRITE:
        downTSR(((Producer) my_running_process).getWriteOkSemID());
        break;
      case UP_ON_READ:
        upTSR(((Producer) my_running_process).getReadOkSemID());
        break;
      case UP_ON_WRITE:
        upTSR(((Consumer) my_running_process).getWriteOkSemID());
        break;
      case IO_REQUEST_1:
      case IO_REQUEST_2:
        ioTSR(the_action);
        break;
      case CAL1:
        calculate();
        break;
      default:
        break;
    }
  }
  
  /**
   * Do some dummy calculations.
   */
  public void calculate()
  {
    int fib = 1;
    int fib_prime = 1;
    for (int i = 1; i < 100; i++)
    {
      final int temp = fib;
      fib += fib_prime;
      fib_prime = temp;
    }
  }

  /**
   * Handle the keyboard interrupt.
   */
  public void keyboardInterrupt()
  {
    synchronized (this)
    {
      setChanged();
      notifyObservers("Keyboard interrupt occured!");
    }
  }

  /**
   * This method is called when timer interrupt or IO interrupt occur.
   * 
   * @param the_object The object that initiated this interrupt.
   * @param the_arg The argument is passed from the_object.
   */
  @Override
  public void update(final Observable the_object, final Object the_arg)
  {
    synchronized (this)
    {
      if (!(my_counter > my_max_counter))
      {
        StringBuffer a_buffer = new StringBuffer(200);
        if (the_object instanceof IODevice)
        {
          final Process a_process =
              my_scheduler.getNextProcessInIOBQ(((IODevice) the_object).getID());
          a_process.setStatus(Status.READY);
          my_scheduler.addProcessToReadyQ(a_process);
          a_buffer.append("IO interrupt : " + my_scheduler.toString() + "\n");
          setChanged();
        }
        else
        {
          if (my_running_process != null)
          {
            my_running_process.setStatus(Status.READY);
            my_scheduler.addProcessToReadyQ(my_running_process);
            my_running_process = null;        
            a_buffer.append("Timer interrupt : " + my_scheduler.toString() + "\n");
            setChanged();
            my_timer.setCountDown(true);
            my_context_switch = true;
          }
        }
        notifyObservers(a_buffer.toString());
      }
    }
  }

  @Override
  public void run()
  {
    /*
     * final Thread timer_thread = new Thread(my_timer); final Thread io1_thread
     * = new Thread(my_io_1); final Thread io2_thread = new Thread(my_io_2);
     * 
     * io1_thread.start(); io2_thread.start(); timer_thread.start();
     */

    while (my_counter <= my_max_counter && my_run)
    {
      my_counter++;
      // setChanged();
      // notifyObservers("My current PC is: " + my_counter);
      synchronized (this)
      {
        if (my_context_switch)
        {
          /*
           * if (my_scheduler.isReadyQueueEmpty()) {
           * System.err.println("Counter: " + my_counter);
           * System.err.println("The ready queue is empty!");
           * System.err.println(my_scheduler.toString()); }
           */
          my_running_process = my_scheduler.getNextReadyProcess();
          my_context_switch = false;
          my_running_process.setStatus(Status.RUNNING);
          setChanged();
          final StringBuffer a_buffer = new StringBuffer(200);
          a_buffer.append("The current PC is : " + my_counter + "\n");
          a_buffer.append("The new running process is : " + my_running_process.toString() +
                          "\n");
          a_buffer.append("The updated queues are : " + my_scheduler.toString());
          notifyObservers(a_buffer.toString());

        }
        final ProcessAction the_action = my_running_process.getAction();
        doAction(the_action);
        my_running_process.advancePC();
      }
    }
    setChanged();
    final StringBuffer a_buffer = new StringBuffer(200);
    a_buffer.append("=========================Program stopped==========================\n");
    a_buffer.append("The PC is : " + my_counter + "\n");
    a_buffer.append("The running process is : " + my_running_process.toString() + "\n");
    a_buffer.append("The queues are : " + my_scheduler.toString());
    notifyObservers(a_buffer.toString());
    my_timer.setIsDone(true);
    my_io_1.setIsDone(true);
    my_io_2.setIsDone(true);

  }
}
